﻿package com.ehouse.connection;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map.Entry;

import android.annotation.SuppressLint;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;

import com.ehouse.R;
import com.ehouse.database.IrrigationScheduleTable;
import com.ehouse.database.IrrigationTable;
import com.ehouse.database.LightScheduleTable;
import com.ehouse.database.LightTable;
import com.ehouse.database.TemperatureScheduleTable;
import com.ehouse.database.TemperatureTable;
import com.ehouse.database.WindowTable;
import com.ehouse.models.Rule;

/**
 * Binding Service to interact with sockets. Info on how to bind <a href=
 * "http://developer.android.com/guide/components/bound-services.html#Binding"
 * >here</a>
 */
@SuppressLint("DefaultLocale")
public class ConnectionService extends Service {
	public static final String WIDGET_CALL_IP = "WIDGET_CALL_IP";
	public static final String WIDGET_SWITCH_SENSOR = "SENSOR";
	public static final String WIDGET_SWITCH_SENSOR_NAME = "SENSOR_NAME";
	public static final String WIDGET_SWITCH_SENSOR_STATE = "SENSOR_STATE";
	public static final String WIDGET_SWITCH_SENSOR_LISTENER = "SENSOR_LISTENER";
	public static final String WIDGET_SELECTION = "WIDGET_SELECTION";
	public static final int TEMP_WIDGET = 0;
	public static final int LIGHT_WIDGET = 1;
	private static final String LOG_TAG = "eHouseConnectionService";
	private final ConnectionBinder binder = new ConnectionBinder();
	private String ip;
	public final int port = 6969;
	private SocketManager manager;

	@Override
	public IBinder onBind(Intent intent) {
		return binder;
	}

	/**
	 * Class used for the client Binder.
	 */
	public class ConnectionBinder extends Binder {

		public ConnectionService getService() {
			return ConnectionService.this;
		}
	}

	/**
	 * Creates a new ConnectionManager service
	 */
	public ConnectionService() {
		this.manager = new SocketManager();
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		if (intent != null && intent.getExtras() != null) {
			final Bundle b = intent.getExtras();
			String ip = b.getString(WIDGET_CALL_IP);
			if (ip != null) {
				openConnection(ip, new IConnectionResponseListener() {
					
					private static final long serialVersionUID = 6503203501738492367L;

					public void responseReceived(Object data, boolean success) {
						if (success) {
							String sensor = b.getString(WIDGET_SWITCH_SENSOR);
							boolean state = b.getBoolean(WIDGET_SWITCH_SENSOR_STATE);
							IConnectionResponseListener listener = (IConnectionResponseListener) b.getSerializable(WIDGET_SWITCH_SENSOR_LISTENER);
							int selection = b.getInt(WIDGET_SELECTION, -1); 
							ArrayList<String> names = b.getStringArrayList(WIDGET_SWITCH_SENSOR_NAME);
							if (selection == TEMP_WIDGET) {
								Log.d(LOG_TAG, "Sending temperature changes from widget");
								switchSensor(sensor, names.get(0), state, listener);
								if (state == true) {
									switchSensor(sensor, names.get(1), false, listener);
								}
								getTemperature(listener);
							} else if (selection == LIGHT_WIDGET) {
								Log.d(LOG_TAG, "Sending light changes from widget");
								for (String name : names) {
									switchSensor(sensor, name, state, listener);
								}
								getLights(listener);
							}
						} else {
							Toast.makeText(ConnectionService.this, R.string.IO_exception, Toast.LENGTH_LONG).show();
						}
					}
				});
			}
		}
		return START_STICKY;
	}

	/**
	 * Starts and opens a connection with the eHouse system
	 * 
	 * @param ip
	 *            The eHouse system IP
	 * @param listener
	 *            Listener to contact whenever the connection process finishes.
	 *            If an error occurs, an Exception will be sent as data,
	 *            containing the error message. Otherwise, data will be null.
	 */
	public void openConnection(String ip,
			final IConnectionResponseListener listener) {
		this.ip = ip;
		new Thread(new Runnable() {

			public void run() {
				try {
					manager.openConnection(listener);
				} catch (UnknownHostException e) {
					Log.w(LOG_TAG, e);
					e.printStackTrace();
					listener.responseReceived(
							new Exception(ConnectionService.this
									.getString(R.string.unknown_host_exception)),
							false);
				} catch (IOException e) {
					Log.w(LOG_TAG, e);
					e.printStackTrace();
					listener.responseReceived(
							new Exception(ConnectionService.this
									.getString(R.string.IO_exception)), false);
				}
			}
		}, "SocketManager open connection").start();
	}

	/**
	 * Closes the connection with the eHouse system
	 */
	public void closeConnection() {
		try {
			manager.closeConnection();
		} catch (Exception e) {
			Log.w(LOG_TAG, e);
			e.printStackTrace();
		}
	}

	/**
	 * Sends to the eHouse system the registration ID so that this device can
	 * receive push notifications. Returns immediately, it will communicate the
	 * received response through the ConnectionResponseListener.
	 * 
	 * @param regId
	 *            Registration id
	 * @param listener
	 *            Listener to which send the received notification. If an error
	 *            occurs, an Exception will be sent as data, containing the
	 *            error message. Otherwise, data will be null.
	 */
	public void sendRegId(final String regId,
			final IConnectionResponseListener listener) {
		new Thread(new Runnable() {

			public void run() {
				try {
					manager.send("REG_ID/" + regId, listener);
				} catch (IOException e) {
					Log.w(LOG_TAG, e);
					e.printStackTrace();
					listener.responseReceived(
							new Exception(ConnectionService.this
									.getString(R.string.IO_exception)), false);
				}
			}
		}, "ConnectionManager sendRegId").start();
	}

	/**
	 * Asks the eHouse system to retrieve the rules for some sensor. Returns
	 * immediately, it will communicate the received response through the
	 * ConnectionResponseListener.
	 * 
	 * @param sensor
	 *            The sensor from which to get the rules
	 * @param listener
	 *            Listener to which send the received notification. If an error
	 *            occurs, an Exception will be sent as data, containing the
	 *            error message. Otherwise, data will be null.
	 */
	public void loadRules(final String sensor,
			final IConnectionResponseListener listener) {
		new Thread(new Runnable() {

			public void run() {
				try {
					manager.send("GET_RULES_" + sensor.toUpperCase(), listener);
				} catch (IOException e) {
					Log.w(LOG_TAG, e);
					e.printStackTrace();
					listener.responseReceived(
							new Exception(ConnectionService.this
									.getString(R.string.IO_exception)), false);
				}
			}
		}, "ConnectionManager loadRules").start();
	}

	/**
	 * Tells the eHouse system to remove the specified rule of a certain sensor.
	 * Returns immediately, it will communicate the received response through
	 * the ConnectionResponseListener.
	 * 
	 * @param sensor
	 *            The sensor from which to remove the rule
	 * @param ruleId
	 *            The id of the rule
	 * @param listener
	 *            Listener to which send the received notification. If an error
	 *            occurs, an Exception will be sent as data, containing the
	 *            error message. Otherwise, data will be null.
	 */
	public void removeRule(final String sensor, final String ruleId,
			final IConnectionResponseListener listener) {
		new Thread(new Runnable() {

			public void run() {
				try {
					manager.send("DELETE_RULES_" + sensor.toUpperCase() + "/"
							+ ruleId.toUpperCase(), listener);
				} catch (IOException e) {
					Log.w(LOG_TAG, e);
					e.printStackTrace();
					listener.responseReceived(
							new Exception(ConnectionService.this
									.getString(R.string.IO_exception)), false);
				}
			}
		}, "ConnectionManager removeRule").start();
	}

	/**
	 * Adds a new light rule to the eHouse system. Returns immediately, it will
	 * communicate the received response through the ConnectionResponseListener.
	 * 
	 * @param rule
	 *            The rule to add to the system
	 * @param listener
	 *            Listener to which send the received notification. If an error
	 *            occurs, an Exception will be sent as data, containing the
	 *            error message. Otherwise, data will be null.
	 */
	public void addLightRule(final Rule rule,
			final IConnectionResponseListener listener) {
		new Thread(new Runnable() {

			@SuppressWarnings("unchecked")
			public void run() {
				try {
					String desc = "";
					HashMap<String, Boolean> dict = (HashMap<String, Boolean>) rule.getData();
					String turn = dict.get("VALUE").toString().toUpperCase();
					for (Entry<String, Boolean> each : dict.entrySet()) {
						if (each.getKey() == "VALUE") {
							continue;
						}
						desc += each.getKey().toUpperCase() + "/"
								+ each.getValue().toString().toUpperCase()
								+ "/";
					}
					String data = rule.getId().toUpperCase() + "\\"
							+ rule.getTime() + "\\"
							+ desc.substring(0, desc.length() - 1) + "\\"
							+ turn;
					rule.setFormatted(data);
					manager.send("SET_RULES_LIGHTS/" + data, listener);
				} catch (IOException e) {
					Log.w(LOG_TAG, e);
					e.printStackTrace();
					listener.responseReceived(
							new Exception(ConnectionService.this
									.getString(R.string.IO_exception)), false);
				}
			}
		}, "ConnectionManager addLightRule").start();

	}

	/**
	 * Adds a new irrigation rule to the eHouse system. Returns immediately, it
	 * will communicate the received response through the
	 * ConnectionResponseListener.
	 * 
	 * @param rule
	 *            The rule to add to the system
	 * @param listener
	 *            Listener to which send the received notification. If an error
	 *            occurs, an Exception will be sent as data, containing the
	 *            error message. Otherwise, data will be null.
	 */
	public void addIrrigationRule(final Rule rule,
			final IConnectionResponseListener listener) {
		new Thread(new Runnable() {

			public void run() {
				try {
					String data = rule.getId().toUpperCase() + "\\"
							+ rule.getTime() + "\\"
							+ rule.getData().toString().toUpperCase();
					rule.setFormatted(data);
					manager.send("SET_RULES_IRRIGATION/" + data, listener);
				} catch (IOException e) {
					Log.w(LOG_TAG, e);
					e.printStackTrace();
					listener.responseReceived(
							new Exception(ConnectionService.this
									.getString(R.string.IO_exception)), false);
				}
			}
		}, "ConnectionManager addIrrigationRule").start();

	}

	/**
	 * Adds a new temperature rule to the eHouse system. Returns immediately, it
	 * will communicate the received response through the
	 * ConnectionResponseListener.
	 * 
	 * @param rule
	 *            The rule to add to the system
	 * @param listener
	 *            Listener to which send the received notification. If an error
	 *            occurs, an Exception will be sent as data, containing the
	 *            error message. Otherwise, data will be null.
	 */
	public void addTemperatureRule(final Rule rule,
			final IConnectionResponseListener listener) {
		new Thread(new Runnable() {

			public void run() {
				try {
					rule.setFormatted(rule.getId().toUpperCase() + "\\" + rule.getData().toString());
					manager.send(
							"SET_RULES_TEMPERATURE/" + rule.getFormatted(),
							listener);
				} catch (IOException e) {
					Log.w(LOG_TAG, e);
					e.printStackTrace();
					listener.responseReceived(
							new Exception(ConnectionService.this
									.getString(R.string.IO_exception)), false);
				}
			}
		}, "ConnectionManager addTemperatureRule").start();

	}

	/**
	 * Retrieves all the lights in the eHouse system. Returns immediately, it
	 * will communicate the received response through the
	 * ConnectionResponseListener.
	 * 
	 * @param listener
	 *            Listener to which send the received notification. The data
	 *            associated to lights is a HashMap<String,Boolean> that denotes
	 *            the different lights and their state. If an error occurs, an
	 *            Exception will be sent as data, containing the error message
	 */
	public void getLights(final IConnectionResponseListener listener) {
		new Thread(new Runnable() {

			public void run() {
				try {
					manager.send("GET_LIGHTS", listener);
				} catch (IOException e) {
					Log.w(LOG_TAG, e);
					e.printStackTrace();
					listener.responseReceived(
							new Exception(ConnectionService.this
									.getString(R.string.IO_exception)), false);
				}
			}
		}, "ConnectionManager getLights").start();
	}

	/**
	 * Retrieves all the windows in the eHouse system. Returns immediately, it
	 * will communicate the received response through the
	 * ConnectionResponseListener.
	 * 
	 * @param listener
	 *            Listener to which send the received notification. The data
	 *            associated to windows is a HashMap<String,Boolean> that
	 *            denotes the different lights and their state. If an error
	 *            occurs, an Exception will be sent as data, containing the
	 *            error message
	 */
	public void getWindows(final IConnectionResponseListener listener) {
		new Thread(new Runnable() {

			public void run() {
				try {
					manager.send("GET_WINDOWS", listener);
				} catch (IOException e) {
					Log.w(LOG_TAG, e);
					e.printStackTrace();
					listener.responseReceived(
							new Exception(ConnectionService.this
									.getString(R.string.IO_exception)), false);
				}
			}
		}, "ConnectionManager getWindows").start();
	}

	/**
	 * Retrieves the temperature in the eHouse system. Returns immediately, it
	 * will communicate the received response through the
	 * ConnectionResponseListener.
	 * 
	 * @param listener
	 *            Listener to which send the received notification. The data
	 *            associated to temperature is a HashMap<String,String>. It
	 *            contains a "VALUE" key that denotes the current temperature and
	 *            two values for which value is a String boolean. If an error
	 *            occurs, an Exception will be sent as data, containing the
	 *            error message
	 */
	public void getTemperature(final IConnectionResponseListener listener) {
		new Thread(new Runnable() {

			public void run() {
				try {
					manager.send("GET_TEMPERATURE", listener);
				} catch (IOException e) {
					Log.w(LOG_TAG, e);
					e.printStackTrace();
					listener.responseReceived(
							new Exception(ConnectionService.this
									.getString(R.string.IO_exception)), false);
				}
			}
		}, "ConnectionManager getTemperature").start();
	}

	/**
	 * Retrieves the irrigation in the eHouse system. Returns immediately, it
	 * will communicate the received response through the
	 * ConnectionResponseListener.
	 * 
	 * @param listener
	 *            Listener to which send the received notification. The data
	 *            associated to irrigation is a String that denotes the state of
	 *            the irrigation. If an error occurs, an Exception will be sent
	 *            as data, containing the error message
	 */
	public void getIrrigation(final IConnectionResponseListener listener) {
		new Thread(new Runnable() {

			public void run() {
				try {
					manager.send("GET_IRRIGATION", listener);
				} catch (IOException e) {
					Log.w(LOG_TAG, e);
					e.printStackTrace();
					listener.responseReceived(
							new Exception(ConnectionService.this
									.getString(R.string.IO_exception)), false);
				}
			}
		}, "ConnectionManager getIrrigation").start();
	}

	/**
	 * Switches a sensor ON or OFF. Returns immediately, it will communicate the
	 * received response through the ConnectionResponseListener.
	 * 
	 * @param sensor
	 *            The type of sensor to switch
	 * @param switchName
	 *            The name of the switch
	 * @param state
	 *            true to turn it ON; false otherwise.
	 * @param listener
	 *            Listener to which send the received notification. If an error
	 *            occurs, an Exception will be sent as data, containing the
	 *            error message. Otherwise, data will be null.
	 */
	public void switchSensor(final String sensor, final String switchName,
			final Boolean state, final IConnectionResponseListener listener) {
		new Thread(new Runnable() {

			public void run() {
				try {
					String sending = "SET_" + sensor.toUpperCase() + "/"
							+ switchName.toUpperCase() + "/"
							+ state.toString().toUpperCase();
					sending = sending.replace("//", "/");

					manager.send(sending, listener);
				} catch (IOException e) {
					Log.w(LOG_TAG, e);
					e.printStackTrace();
					listener.responseReceived(
							new Exception(ConnectionService.this
									.getString(R.string.IO_exception)), false);
				}
			}
		}, "ConnectionManager switchSensor").start();
	}

	/**
	 * 
	 * Interface to implement if it is to receive notifications about the
	 * responses of the eHouse system.
	 * 
	 */
	public interface IConnectionResponseListener extends java.io.Serializable {
		public void responseReceived(Object data, boolean success);
	}

	private class SocketManager {
		private Socket socket;
		private PrintWriter printer;
		private BufferedReader reader;
		private Object lock = new Object();

		public void send(String text, IConnectionResponseListener listener)
				throws IOException {
			Log.d(LOG_TAG, "Sending message: " + text);
			StringBuffer sb;
			synchronized (lock) {
				printer.print(text);
				printer.flush();
				char[] buffer = new char[1024];
				int read = reader.read(buffer);
				sb = new StringBuffer(read);
				for (int i = 0; i < read; i++) {
					sb.append(buffer[i]);
				}
			}
			parseText(sb.toString(), listener);
		}

		private void parseText(String text, IConnectionResponseListener listener) {
			Log.d(LOG_TAG, "Received message: " + text);
			String[] split = text.split("/");
			Object response = null;
			boolean success = true;

			try {
				if (split[0].equals("QUIT")) {
					return;
				} else if (split[0].startsWith("HELLO")) {
					response = null;
				} else if (split[0].equals("CORRECT")) {
					response = null;
				} else if (split[0].equals("TEMPERATURE")) {
					HashMap<String, String> tempSensors = new HashMap<String, String>();
					tempSensors.put("VALUE", split[1]);
					for (int i = 2; i < split.length; i += 2) {
						tempSensors.put(split[i], split[i + 1]);
					}
					response = tempSensors;
					TemperatureTable.update(getContentResolver(), tempSensors);
				} else if (split[0].equals("IRRIGATION")) {
					response = split[1];
					IrrigationTable.update(getContentResolver(), new int[]{Boolean.parseBoolean(split[1]) ? 1 : 0});
				} else if (split[0].equals("LIGHTS")) {
					HashMap<String, Boolean> lights = new HashMap<String, Boolean>();
					for (int i = 1; i < split.length; i += 2) {
						lights.put(split[i], split[i + 1].toUpperCase().equals("TRUE"));
					}
					response = lights;
					LightTable.update(getContentResolver(), lights);
				} else if (split[0].equals("WINDOWS")) {
					HashMap<String, Boolean> windows = new HashMap<String, Boolean>();
					for (int i = 1; i < split.length; i += 2) {
						windows.put(split[i], split[i + 1].toUpperCase().equals("TRUE"));
					}
					response = windows;
					WindowTable.update(getContentResolver(), windows);
				} else if (split[0].equals("RULES_LIGHT")) {
					Collection<Rule> rules = new ArrayList<Rule>();
					String[] mainsplit = text.substring(text.indexOf('/') + 1)
							.split("\\|");
					for (int i = 0; i < mainsplit.length; i++) {
						if (mainsplit[i] == "") {
							continue;
						}
						String[] smallsplit = mainsplit[i].split("\\\\");
						Rule r = new Rule(smallsplit[0], smallsplit[1],
								mainsplit[i], null);
						String[] roomsplit = smallsplit[2].split("/");
						HashMap<String, Boolean> dict = new HashMap<String, Boolean>();
						for (int j = 0; j < roomsplit.length; j += 2) {
							dict.put(roomsplit[j].substring(0, 1)
									+ roomsplit[j].substring(1).toLowerCase(),
									roomsplit[j + 1].toUpperCase().equals("TRUE"));
						}
						dict.put("VALUE", roomsplit[3].toUpperCase().equals("TRUE"));
						r.setData(dict);
						rules.add(r);
					}
					response = rules;
					LightScheduleTable.update(getContentResolver(), rules);
				} else if (split[0].equals("RULES_IRRIGATION")) {
					Collection<Rule> rules = new ArrayList<Rule>();
					String[] mainsplit = text.substring(text.indexOf('/') + 1)
							.split("\\|");
					for (int i = 0; i < mainsplit.length; i++) {
						if (mainsplit[i] == "") {
							continue;
						}
						String[] smallsplit = mainsplit[i].split("\\\\");
						Rule r = new Rule(smallsplit[0], smallsplit[1],
								mainsplit[i], smallsplit[2].toUpperCase().equals("TRUE"));

						rules.add(r);
					}
					response = rules;
					IrrigationScheduleTable.update(getContentResolver(), rules);
				} else if (split[0].equals("RULES_TEMPERATURE")) {
					String[] mainsplit = text.substring(text.indexOf('/') + 1)
							.split("\\\\");
					Collection<Rule> rules = new ArrayList<Rule>();
					if (mainsplit.length > 1) {

						rules.add(new Rule(mainsplit[0], mainsplit[1],
								text.substring(text.indexOf('/') + 1),
								mainsplit[1]));
					}
					response = rules;
					TemperatureScheduleTable.update(getContentResolver(), rules);
				} else {
					Log.w(LOG_TAG, "Command not identified: "
							+ text);
					success = false;
					response = new Exception(
							ConnectionService.this
									.getString(R.string.parse_error) + text);
				}
			} catch (Exception ex) {
				Log.w(LOG_TAG, ex);
				ex.printStackTrace();
				success = false;
				response = new Exception(
						ConnectionService.this.getString(R.string.parse_error)
								+ text);
			}

			listener.responseReceived(response, success);
		}

		public void openConnection(IConnectionResponseListener listener)
				throws IOException {
			socket = new Socket(ip, port);
			printer = new PrintWriter(socket.getOutputStream());
			reader = new BufferedReader(new InputStreamReader(
					socket.getInputStream()));
			// String id = ConnectionManager.this.getSharedPreferences(
			// settingsKey, Context.MODE_PRIVATE).getString(
			// NotificationsService.REG_ID, "");
			// send("HELLO SERVER/" + id, listener);
			send("HELLO_SERVER", listener);
		}

		public void closeConnection() throws IOException {
			if (socket.isConnected()) {
				this.send("QUIT", null);
				socket.close();
			}
		}
	}
}